Skip to content

fix(airplay): send X-Apple-HKP so pair-setup gets past 400 - #41

Merged
Developer1010x merged 1 commit into
masterfrom
fix/airplay-hkp-header
Aug 26, 2026
Merged

fix(airplay): send X-Apple-HKP so pair-setup gets past 400#41
Developer1010x merged 1 commit into
masterfrom
fix/airplay-hkp-header

Conversation

@snadahalli

Copy link
Copy Markdown
Owner

Symptom

With AirPlay Receiver set to Everyone and Require Password off, POST /pair-setup returned 400 Bad Request with an empty body — before the receiver looked at the TLV8 payload at all. GET /info succeeded against the same host, so this was not access control.

This is the third distinct wall on the way to #27, after 403 (AirPlay Receiver set to "Current User") and 470 (Require Password on).

Isolating it

Replayed the request against a Mac running AirTunes/950.7.1, changing one variable at a time:

Request Result
no Host, FLAGS=0x02 u8 — what this code sent 400
+ Host 400
+ Host, FLAGS=0x10 u32 400
+ Host, X-Apple-HKP: 4, FLAGS=0x10 u32 200, body 409B
+ Host, X-Apple-HKP: 4, no FLAGS 200, body 409B
no Host, X-Apple-HKP: 4, FLAGS=0x10 u32 200, body 409B

X-Apple-HKP is the only variable that matters. It selects the pairing flow, so without it the endpoint rejects the request outright.

Two theories this disproves, both of which looked compelling:

  • The missing Host header. HTTP/1.1 requires it and RFC 7230 §5.4 says a server MUST answer 400 without it — but adding it changes nothing, and the request succeeds without it once X-Apple-HKP is present.
  • The FLAGS value. The code sends 0x02 in one byte where HAP defines kPairingFlag_Transient as 0x00000010 in a uint32. The receiver ignores the TLV completely — 0x02, a correct 0x10, and omitting it entirely all produce an identical M2.

Since the flag demonstrably does not matter, it is left as-is rather than changed on spec-reading alone; the comment now records the measurement instead of the previous incorrect claim that 0x02 is the transient flag.

Bonus confirmation for #23

The 409-byte M2 decodes as state + 16-byte salt + 384-byte public key. 384 bytes is 3072 bits — independent confirmation that the receiver uses the same RFC 5054 group that #23 installed, from the receiver's own wire output rather than from our tests.

What this does not fix

pair_probe now reaches a genuine M2:

DEBUG M2 received server_pk_len=384 salt_len=16
❌ PAIR-SETUP FAILED: HAP error 2: Authentication failed

So the SRP exchange finally happens, and fails at M4 with the receiver rejecting our client proof. That is a separate bug in the proof computation or the transient password, and is not addressed here.

I probed two candidate divergences (H(g) over PAD(g) vs. minimal encoding, and K = H(S) padded vs. not); both reachable variants still returned error 2, and the rest were masked — see below.

Warning for whoever picks up M4

The receiver backs off hard after a failed pair-setup, answering error 0x03 with a retry delay. Attempts less than a couple of minutes apart return backoff rather than a real answer, and backoff looks nothing like an authentication failure — this silently invalidated a first round of my own results before I noticed. Space attempts generously when bisecting.

Verification

fmt --check clean, clippy --all-targets --all-features -D warnings clean, cargo test --all 216 passed / 0 failed, plus the wire captures above against real hardware.

With AirPlay Receiver set to Everyone and no password, `POST /pair-setup`
returned 400 Bad Request with an empty body — before the receiver looked at
the TLV8 payload at all. `GET /info` succeeded, so this was not access
control.

Isolated by replaying the request against a Mac running AirTunes/950.7.1,
changing one thing at a time:

    no Host, FLAGS=0x02 u8      (what this code sent)   400
    + Host                                              400
    + Host, FLAGS=0x10 u32                              400
    + Host, X-Apple-HKP: 4, FLAGS=0x10 u32              200, body 409B
    + Host, X-Apple-HKP: 4, no FLAGS                    200, body 409B
    no Host, X-Apple-HKP: 4, FLAGS=0x10 u32             200, body 409B

`X-Apple-HKP` is the only variable that matters: present, the receiver
answers; absent, 400 every time. It selects the pairing flow, so without it
the endpoint rejects the request outright. `Host` is irrelevant here despite
HTTP/1.1 requiring it, and the receiver ignores the `FLAGS` TLV completely.

The 409-byte M2 decodes as state + 16-byte salt + 384-byte public key. 384
bytes is 3072 bits, which is independent confirmation that the receiver uses
the same RFC 5054 group #23 installed.

`pair_probe` now gets a real M2 ("server_pk_len=384 salt_len=16") and fails
later, at M4, with HAP error 2 (authentication) — the receiver rejecting our
SRP proof. That is a different bug and is not addressed here; see the issue.

Note when re-testing: the receiver backs off hard after a failed pair-setup
and answers with error 0x03 and a retry delay. Attempts less than a couple of
minutes apart return backoff rather than a real answer, which will silently
mislead anyone bisecting this.
@Developer1010x
Developer1010x merged commit f02230a into master Aug 26, 2026
5 checks passed
Developer1010x pushed a commit that referenced this pull request Aug 26, 2026
…43)

#42 was merged twelve seconds after #41, into `fix/airplay-hkp-header` —
which #41 had just merged into master and left behind. The merge succeeded,
so nothing looked wrong, but the commits landed on a branch nothing points at
and master never received them.

Master therefore has the `X-Apple-HKP` header from #41 and none of what it was
a prerequisite for: the SRP proof still hashes g padded, transient pairing
still runs M5/M6 and gets the connection closed, and there is no encrypted
control channel. Pairing is broken on master in exactly the way #42 fixed.

This restores #42's own diff — the eight `openplay-airplay` files it actually
touched — on top of current master.

Deliberately *not* a merge of `fix/airplay-hkp-header`. That branch was cut
before #35, #36, #37, #39 and #40 merged, so a diff against it reads as
deleting `openplay-discovery/src/address.rs` and reverting 244 lines of
`openplay-sender/src/app.rs`. Merging it would silently undo five landed
fixes. Only the range between #42 and its own parent is safe to replay, and
that range touches nothing outside `openplay-airplay`.

Verified after the replay: 233 tests pass, clippy and fmt clean, and the work
from #35/#36/#37/#40 is still in the tree.

Co-authored-by: Sandeepa Nadahalli <1698507+snadahalli@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants